package com.itextpdf.text.pdf;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.HashSet;

import com.itextpdf.text.DocumentException;

import kr.co.m2soft.intern.UInt;


/**
 * Make Font subset from SubsetFont.class
 * FontSubset is generated in two cases: OTF and TTF
 * 
 * @author dogfooter
 *
 * 
 */

public class FontSubsetGenerator {

	/**
	 * method that makes Font subset and save to the file system 
	 * 
	 * @param fontFileName
	 *            direct/indirect route of source font file which will became subset font
	 * @param targetPath
	 *            direct route for path(it should be directory name, not file name) to save the font subset
	 * @param text
	 *            text for making subset
	 *            
	 * 
	 * @return
	 * 			final target subset file's absolute path
	 * 
	 * @throws IOException 
	 * @throws DocumentException 
	 */
	public static String GEN(String fontFileName, String targetPath, String text) throws  DocumentException, IOException {

		if (!targetPath.toLowerCase().endsWith(".otf") && !targetPath.toLowerCase().endsWith(".ttf")) {
			throw new IOException("Target path error : You should attach filename with extension .otf or .ttf");
		}
		
		if (text == "" || text == null) {
			throw new DocumentException("GEN() parameter String text should not be empty"); 
		}
		
		
		SubsetFont subsetFont = new SubsetFont(fontFileName, text);
		TrueTypeFontUnicode font = subsetFont.getFont();

		String finalTargetPathName = "";
		byte[] ret = null;
		int differenceOfLength = 0;

		if (subsetFont.getActualFileName().toLowerCase().endsWith(".otf")) {
			// case of OTF
			byte unprocessedCFF[] = null;
			byte processedCFF[] = null;
			byte tableBeforeCFF[] = null;
			byte tablesAfterCFF[] = null;
			try {
				unprocessedCFF = font.readCffFont();
				processedCFF = null;
				tableBeforeCFF = font.readTablesBeforeCFF();
				tablesAfterCFF = font.readTablesAfterCFF();
			} catch (IOException e) {
				
				throw new IOException("font.readCff rf", e.getCause());
			}

			// make CFF subset by handing over longTag
			@SuppressWarnings("deprecation")
			CFFFontSubset cff = new CFFFontSubset(new RandomAccessFileOrArray(unprocessedCFF), subsetFont.getLongTag());
			try {
				// Process()is the main method to make CFF subset binary
				processedCFF = cff.Process(cff.getNames()[0]);
				differenceOfLength = unprocessedCFF.length - processedCFF.length;
			} catch (Exception e) {
				font.setSubset(false);
			}

			//  change 1) the length and checksum of CFF and 2)offset of other tables which their offsets are changed by the changer CFF subset
			tableBeforeCFF = correctTableRecordCausedByCFF(tableBeforeCFF, makeChecksum(processedCFF),
					new UInt((long) processedCFF.length).getBytes(), (long) differenceOfLength);

			// mix each parts to make whole otf file 
			ret = new byte[processedCFF.length + tableBeforeCFF.length + tablesAfterCFF.length];
			System.arraycopy(tableBeforeCFF, 0, ret, 0, tableBeforeCFF.length);
			System.arraycopy(processedCFF, 0, ret, tableBeforeCFF.length, processedCFF.length);
			System.arraycopy(tablesAfterCFF, 0, ret, processedCFF.length + tableBeforeCFF.length,
					tablesAfterCFF.length);

		} else if (subsetFont.getActualFileName().toLowerCase().endsWith(".ttf")) {
			// case TTF
			synchronized (font.rf) {
				@SuppressWarnings("deprecation")
				TrueTypeFontSubSet sb = new TrueTypeFontSubSet(font.fileName, new RandomAccessFileOrArray(font.rf),
						new HashSet<Integer>(subsetFont.getLongTag().keySet()), font.directoryOffset, true, true);
				// process()is the main method to make subset
				try {
					ret = sb.process();
				} catch (DocumentException e) {
					// TODO Auto-generated catch block
					throw new DocumentException("TrueTypeFontSubSet process failed", e);
				}
			}

		}

		
		if (!targetPath.contains("/") && !targetPath.contains("\\")) {
			// It has only file name : save in directory that same as source font file for
			finalTargetPathName = subsetFont.getDirectory() + subsetFont.getPathDelimiter() + targetPath;
		} else {
			finalTargetPathName = targetPath;
		}
		
		FileOutputStream fos = null;
		try {
			fos = new FileOutputStream(finalTargetPathName);
			fos.write(ret);
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			
			throw new IOException("file output failed", e.getCause());
		} finally {
			if (fos != null)
				fos.close();
		}

		return Paths.get(finalTargetPathName).toAbsolutePath().toString();
	}

	/**
	 * 
	 * @param beforeCFF
	 *         byte array that will get 1)tables that is located before CFF table and 2)table Record
	 * @param checksum
	 *            checksum of new CFF
	 * @param length
	 *            lenfth of new CFF
	 * @param differenceOfLength
	 *            unprocessedLengthOfCFF - processedLengthOfCFF
	 * @return bytes that contains changed tables and table records which are located before CFF table 
	 */
	private static byte[] correctTableRecordCausedByCFF(byte[] beforeCFF, byte[] checksum, byte[] length,
			long differenceOfLength) {

		final int TABLE_SIZE = 16;
		final long NUM_TABLE = ((beforeCFF[4] & 0xff) << 8) + (beforeCFF[5] & 0xff);
		int checksumIndex = 0, lengthIndext = 0, offsetIndex = 0;
		long CFFOffset = 0, tmp = 0;
		byte[] changedOffset = new byte[4];

		// fint CFF table from table record
		for (int i = 0; i < beforeCFF.length; i += 4) {
			if (beforeCFF[i] == 67 && beforeCFF[i + 1] == 70 && beforeCFF[i + 2] == 70 && beforeCFF[i + 3] == 32) {
				// found CFF

				// get 1)offset binary that contains offset, 2)length, 3)checksum of cff from table record
				checksumIndex = i + 4;
				lengthIndext = i + 12;
				offsetIndex = i + 8;
				break;
			}
		}

		// modify checksum
		for (int i = 0; i < checksum.length; i++) {
			beforeCFF[checksumIndex + i] = checksum[i];
		}

		// modify length
		for (int i = 0; i < length.length; i++) {
			beforeCFF[lengthIndext + i] = length[i];
		}

		// get offset of CFF
		for (int i = 0; i < 4; i++) {
			CFFOffset += (((long) beforeCFF[offsetIndex + i] & 0xFF) & 0xffffffff) << (3 - i) * 8;
		}

		// after get tables that is located before CFF table(which means table offset > CFF offset) and modify
		for (int i = 0; i < NUM_TABLE; i++) {

			// get new offset of each table
			tmp = 0;
			for (int j = 0; j < 4; j++) {
				tmp += (((long) beforeCFF[12 + 8 + i * TABLE_SIZE + j] & 0xFF) & 0xffffffff) << (3 - j) * 8;
			}

			if (CFFOffset < tmp) {
				// table offset is larger than CFF table offset
				changedOffset = (new UInt(tmp - differenceOfLength)).getBytes();
				for (int j = 0; j < changedOffset.length; j++) {
					beforeCFF[12 + 8 + i * TABLE_SIZE + j] = changedOffset[j];
				}
			}
		}
		return beforeCFF;
	}

	/**
	 * 
	 * @param b
	 *         byte array that will make checksum
	 * @return byte array that contains checksum
	 */
	private static byte[] makeChecksum(byte[] b) {

		long sum = 0 & 0xffffffff;
		long tmp;
		UInt uint32 = null;

		try {
			for (int k = 0; k < b.length; k += 4) {
				if (b.length - k <= 3) {
					for (int j = 0; j < b.length - k; j++) {
						tmp = ((long) b[k + j] & 0xFF) & 0xffffffff;
						tmp = tmp << ((3 - j) * 8);
						sum = (sum + tmp) & 0xffffffff;
					}
				} else {
					for (int j = 0; j < 4; j++) {
						tmp = ((long) b[k + j] & 0xFF) & 0xffffffff;
						tmp = tmp << ((3 - j) * 8);
						sum = (sum + tmp) & 0xffffffff;
					}

				}
			}
			uint32 = new UInt(sum);
		} catch (Exception e) {
		}
		return uint32.getBytes();
	}

	// /**
	// * 예전 가설 중 hmtx table이 CFF 보다 뒤에 있어 문제가 생겼다고 생각. 그러나 너무나 예외가 많았다. 또한 이를 통해 성공한
	// * 사례가 없다. 메소드를 만들어만 놓고 사용하지는 않음
	// *
	// * 메소드의 기능은 단순히 hmtx table을 CFF 앞으로 옮기고 table record를 수정한다.
	// *
	// * @param whole
	// * 모든 값이 담긴 바이트 배열
	// * @return table들의 위치가 바뀐 바이트 배열
	// */
	// private static byte[] moveHmtx(byte[] whole) {
	//
	// final int TABLE_SIZE = 16;
	// final long NUM_TABLE = ((whole[4] & 0xff) << 8) + (whole[5] & 0xff);
	//
	// byte ret[] = new byte[whole.length];
	//
	// long offsetHmtx = 0;
	// long lengthHmtx = 0;
	// long offsetCFF = 0;
	// long lengthCFF = 0;
	//
	// long tmpOffset = 0;
	//
	// // find hmtx
	// for (int i = 0; i < NUM_TABLE; i++) {
	// if (whole[i * TABLE_SIZE + 12] == 104 && whole[i * TABLE_SIZE + 12 + 1] ==
	// 109
	// && whole[i * TABLE_SIZE + 12 + 2] == 116 && whole[i * TABLE_SIZE + 12 + 3] ==
	// 120) {
	// // hmtx found
	// for (int k = 0; k < 4; k++) {
	// offsetHmtx += (((long) whole[i * TABLE_SIZE + 12 + 8 + k] & 0xFF) &
	// 0xffffffff) << (3 - k) * 8;
	// lengthHmtx += (((long) whole[i * TABLE_SIZE + 12 + 12 + k] & 0xFF) &
	// 0xffffffff) << (3 - k) * 8;
	// }
	// } else if (whole[i * TABLE_SIZE + 12] == 67 && whole[i * TABLE_SIZE + 12 + 1]
	// == 70
	// && whole[i * TABLE_SIZE + 12 + 2] == 70 && whole[i * TABLE_SIZE + 12 + 3] ==
	// 32) {
	// // hmtx found
	// for (int k = 0; k < 4; k++) {
	// offsetCFF += (((long) whole[i * TABLE_SIZE + 12 + 8 + k] & 0xFF) &
	// 0xffffffff) << (3 - k) * 8;
	// lengthCFF += (((long) whole[i * TABLE_SIZE + 12 + 12 + k] & 0xFF) &
	// 0xffffffff) << (3 - k) * 8;
	// }
	//
	// }
	// }
	//
	// if (offsetHmtx < offsetCFF)
	// return null;
	//
	// byte hmtx[] = new byte[(int) lengthHmtx];
	// byte CFF[] = new byte[(int) lengthCFF];
	// byte beforeCFF[] = new byte[(int) offsetCFF];
	// byte aCbH[] = new byte[(int) (offsetHmtx - (offsetCFF + lengthCFF))];
	// byte afterHmtx[] = new byte[(int) (whole.length - (offsetHmtx +
	// lengthHmtx))];
	//
	// System.arraycopy(whole, 0, beforeCFF, 0, beforeCFF.length);
	// System.arraycopy(whole, (int) offsetCFF, CFF, 0, CFF.length);
	// System.arraycopy(whole, (int) (offsetCFF + lengthCFF), aCbH, 0, aCbH.length);
	// System.arraycopy(whole, (int) offsetHmtx, hmtx, 0, hmtx.length);
	// System.arraycopy(whole, (int) (offsetHmtx + lengthHmtx), afterHmtx, 0,
	// afterHmtx.length);
	//
	// byte[] tmpArr = null;
	// // long diffOffset = Math.abs(offsetCFF - offsetHmtx);
	// for (int i = 0; i < NUM_TABLE; i++) {
	// tmpOffset = 0;
	// for (int k = 0; k < 4; k++) {
	// tmpOffset += (((long) whole[i * TABLE_SIZE + 12 + 8 + k] & 0xFF) &
	// 0xffffffff) << (3 - k) * 8;
	// }
	//
	// if (tmpOffset == offsetCFF) {
	//
	// tmpArr = (new UInt(offsetCFF + lengthHmtx)).getBytes();
	// for (int j = 0; j < tmpArr.length; j++) {
	// beforeCFF[i * TABLE_SIZE + j + 12 + 8] = tmpArr[j];
	// }
	//
	// } else if (tmpOffset == offsetHmtx) {
	//
	// tmpArr = (new UInt(offsetCFF)).getBytes();
	// for (int j = 0; j < tmpArr.length; j++) {
	// beforeCFF[i * TABLE_SIZE + j + 12 + 8] = tmpArr[j];
	// }
	//
	// } else if (tmpOffset < (offsetHmtx) && tmpOffset > offsetCFF) {
	//
	// tmpArr = (new UInt(tmpOffset + lengthHmtx)).getBytes();
	// for (int j = 0; j < tmpArr.length; j++) {
	// beforeCFF[i * TABLE_SIZE + j + 12 + 8] = tmpArr[j];
	// }
	//
	// }
	//
	// }
	//
	// System.arraycopy(beforeCFF, 0, ret, 0, beforeCFF.length);
	// System.arraycopy(hmtx, 0, ret, beforeCFF.length, hmtx.length);
	// System.arraycopy(CFF, 0, ret, beforeCFF.length + hmtx.length, CFF.length);
	// System.arraycopy(aCbH, 0, ret, beforeCFF.length + hmtx.length + CFF.length,
	// aCbH.length);
	// System.arraycopy(afterHmtx, 0, ret, beforeCFF.length + hmtx.length +
	// CFF.length + aCbH.length,
	// afterHmtx.length);
	//
	// return ret;
	// }
}
